【VS Code+Qt6】拖放操作

您所在的位置:网站首页 qlabel 设置颜色 【VS Code+Qt6】拖放操作

【VS Code+Qt6】拖放操作

2023-05-31 19:01| 来源: 网络整理| 查看: 265

cmake_minimum_required(VERSION 3.20) project(DragDemo LANGUAGES CXX) set(CMAKE_AUTOMOC ON) find_package( Qt6 COMPONENTS Core Gui Widgets REQUIRED ) # 找到项目下所有头文件和源文件 file(GLOB_RECURSE SRC_LIST include/*.h src/*.cpp) include_directories(include) add_executable(DragDemo WIN32 ${SRC_LIST}) target_link_libraries( DragDemo PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets )

代码插件没有 CMake 的,老周用的是 C++ 的插入,因为里面出现了 /*,被识别成了注释,所以上面内容后半部分全绿了。

项目结构是这样的:

 

下面是头文件。

#pragma once #include #include #include class Demo : public QWidget { Q_OBJECT public: Demo(QWidget* parent=nullptr); protected: void paintEvent(QPaintEvent* event) override; void mousePressEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; private: // 这个私有变量用来临时存储鼠标按下的坐标 QPoint m_curpt; };

这个类没什么特别的,就是一个自定义窗口。其中,重写 paintEvent 方法,在窗口上画提示文字。这个只为了好看,你可以省略。

重点是重写 mousePress 和 mouseMove 两个事件,mousePress 时记下鼠标按下的坐标,然后在 mouseMove 中再次获取鼠标的坐标,和按下时的坐标相减,看看它们的曼哈顿距离是否符合启动拖放的条件。

咱们来看实现代码。

Demo::Demo(QWidget *parent) { this->setWindowTitle("拖动示例"); this->resize(258, 240); this->move(659, 520); } void Demo::paintEvent(QPaintEvent *event) { QRect rect=event->rect(); // 在窗口上绘制文本 QFont font; font.setFamily("华文仿宋"); //字体名称 font.setPointSize(24); //字体大小(点) font.setBold(true); //加粗 QPainter painter(this); // 设置字体 painter.setFont(font); // 计算一下文本所占空间 QString textToDraw = "从此窗口拖动"; QRect textRect = painter.fontMetrics().boundingRect(rect, Qt::AlignCenter, textToDraw); // 移动文本矩形,让它的中心点和窗口矩形的中心点对齐 textRect.moveCenter(rect.center()); // 设置绘制文本的画笔 QPen pen; pen.setColor(QColor("red")); painter.setPen(pen); // 开始涂鸦 painter.drawText(textRect.toRectF(), textToDraw); painter.end(); } void Demo::mousePressEvent(QMouseEvent *event) { // 获取鼠标按下的坐标点 m_curpt = event->pos(); } void Demo::mouseMoveEvent(QMouseEvent *event) { // 获取鼠标现在的位置坐标 QPoint curloc = event->pos(); // 和刚才按下去的坐标比较 if((m_curpt - curloc).manhattanLength() setText(str); // 发快递 // QDrag(QObject *dragSource) // dragSource 指的是发起拖放操作的对象 // 这里是当前窗口 QDrag drag(this); // 设置数据 drag.setMimeData(mdata); // 出发 auto result = drag.exec(Qt::CopyAction | Qt::LinkAction, Qt::CopyAction); QString displaymsg = "数据传递完毕,操作结果:"; if(result & Qt::CopyAction) { displaymsg += "复制"; } else if(result & Qt::LinkAction) { displaymsg += "链接"; } else if(result & Qt::IgnoreAction) { displaymsg += "忽略"; } else { displaymsg += "未知"; } QMessageBox::information(this, "提示", displaymsg, QMessageBox::Ok); }

paintEvent 的重写不是重点,不过老周简单说下。

a、创建 QFont 实例,你看名字都知道是什么鬼了,是的,设置字体参数;

b、计算文本”从此窗口拖动“要占多少空间,核心是调用 QFontMetrics 类的 boundingRect 方法。这里要注意,调用的是这个重载:

QRect QFontMetrics::boundingRect(const QRect &r, int flags, const QString &text, int tabstops = 0, int *tabarray = (int *)nullptr) const

也就是说,不能调用只传文本的重载,那个重载计算出来的 rect 宽度会变小,导致绘制出来的字符串少了一个字符(原因不明)。但,调用上面这个有N多参数的重载是没问题。区别就在于给也一个 r 参数,这个参数提供一个矩形区域作为约束。这里老周用整个窗口的空间作为约束。可能是给的空间足够大,所以计算出来的宽度就足够。于是老周厚着脸皮翻了一下 Qt 的源码,这两重载所使用的处理方法不一样,参数比较多的那个里面调用的是 qt_format_text 函数,参数较少的那个里面用的是 QStackTextEngine 类。有兴趣的伙伴可以去翻翻。

moveCenter 是使矩形平移,并且中心点对准窗口矩形区域的中心点。这里可以让绘制的文本处在窗口的中央。

接下来说说  mousePress 事件,这里就很简单了,就是直接记录鼠标的位置。不过,有点不严谨,拖放操作没听说过用鼠标右键操作的吧?所以,此处最好判断一下,是不是左键按下。

void Demo::mousePressEvent(QMouseEvent *event) { if(!(event -> buttons() & Qt::LeftButton)) { return; } // 获取鼠标按下的坐标点 m_curpt = event->pos(); }

mouseMove 事件也是如此。

void Demo::mouseMoveEvent(QMouseEvent *event) { if(!(event -> buttons() & Qt::LeftButton)) { return; } …… }

QDrag::exec 方法是在 mouseMove 事件中启动的,这个就和剪贴板的操作相似了。先创建 QMimeData,设置文本数据,然后创建 QDrag 实例,设置 MimeData,然后就调用 exec 方法。

最后是整个程序的 main 函数。

int main(int argc, char* argv[]) { QApplication app(argc, argv); Demo window; window.show(); return QApplication::exec(); }

运行示例后,打开一个文本编辑器(如记事本),在窗口上按下鼠标左键,拖到文本编辑器,文本就发送到目标窗口了。

 

然后,咱们来看 drop 操作。

要想让某个组件支持放置行为,你必须调用:

setAcceptDrops(true);

默认是不开启的,所以必须调用一次 setAcceptDrops 方法。

当某个组件(可以是窗口,按钮,标签,文本框等)支持放置行为后,把数据拖到该组件上会引发 dragEnter、dragMove 等事件;释放鼠标时会发生 drop 事件,表示整个拖放操作结束。这个上文已讲过,下面重点看几个事件参数。注意了,这几个厮实际上是有继承关系的。

class QDropEvent : public QEvent class QDragMoveEvent : public QDropEvent class QDragEnterEvent : public QDragMoveEvent // 下面这个是特例 class QDragLeaveEvent : public QEvent

QDragLeaveEvent 是直接派生自 QEvent 的,因为它是在 dragLeave 事件发生时使用,数据被拖出当前对象,一般不需要额外携带什么参数,所以这个事件类比较特殊。

QDropEvent 类用于 drop 事件,因为这时候你得读取数据了,所以它会夹带私货。这些私货分两类:

1、跟鼠标有关的。比如 buttons 返回鼠标按下了哪个键;modifiers 返回值表示用户是否在拖动的同时按下 Ctrl、Alt、Shift 等按键。position 返回鼠标指针的当前坐标。这些参数咱们通常用不上的。

2、和共享的数据相关的。这个是最需要的。mimeData 返回 QMimeData 对象的指针,然后咱们就能读数据了。source 返回发起拖放操作的对象,一般我们的程序不太关注数据源。

不管读不读取数据,作为数据接收者,我们是文明的,有礼貌的。拖放操作完成时咱们应该响应一下发送者—— QDrag::exec 方法(如果数据是从其他程序拖过来的,那么,拖放的发起者就不一定是调用 exec 方法,毕竟人家不见得是用 Qt 写的,说不定是用 WPF 做的)。

扯远了,回到主题,向数据发送者反馈,还是涉及到了 DropActions 的事。DropEvent 提供了这些成员,可以访问 action。

1、possibleActions 方法,对应的是 exec 方法的 supportedActions 参数;

2、proposedAction 方法,对应 exec 方法的 defaultAction 参数。

还记得前文说过的 exec 方法的两个参数吗?嗯,是滴,possibleActions 就是 supportedActions 参数提供的有效范围,你只能在这些值中选一个。proposedAction 是建议的值,也就是 defaultAction 参数提供的默认值。

所以,如果我们的程序比较在意使用什么 action 的话,你得好好分析一下这两个方法返回的值了。不过,多数时候,我们只关心 mimeData 返回的内容,因为那是要提取的数据。

如果你成功接收了数据,那么要调用 acceptProposedAction 方法,表示数据和 defaultAction 你都接受了。

如果你不想用 defaultAction 参数推荐的默认 action,那么,你可以调用 QDropEvent::setDropAction 方法自己设置一个 action,但你设置的 action 必须在 possibleActions 中允许的。如果你调用了 setDropAction 方法,就等于修改了默认 action,所以这时候你只能调用 accept 方法来接受,不能再调用 acceptProposedAction 方法了。不然,acceptProposedAction 方法会还原默认 action 的值。

如果你发现数据不是你想要的,或者数据发送者给的 DropAction 你不接受,那你就调用 ignore 方法忽略,或者你什么都不做也可以(默认会 ignore 掉事件)。

QDragEnterEvent 和 QDragMoveEvent 都是 QDropEvent 的子类,所以成员都是差不多的。就不用老周再废话了。

了解这几个类的关系,你就知道怎么处理接收拖动的过程了。下面我们来个例子,把图片文件拖到咱们的程序,然后会显示该图片。就是拖动打开文件了。

从 QLabel 类派生出一个类,咱们就用它来接收并显示图片。Qt 没有专门显示图片的组件,一般用 QLabel 来显示图片。当然,QPushButton 等按钮组件也可以显示图片,不过通常用作显示小图标。有大伙伴会说,QGraphicsView 什么什么的不用吗?那个就太大动作了,简直是杀小强用牛刀,没有必须,我就想显示个图片而已。

#pragma once #include #include #include #include class MyLabel : public QLabel { Q_OBJECT public: MyLabel(QWidget* parent=nullptr); protected: void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent* event) override; void resizeEvent(QResizeEvent* event) override; private: // 用来缓存图像 QPixmap m_image; };

事件不算多,就重写三个事件。另外,还声明了一个私有成员 m_image 用来存图像资源。你可能会问了,QLabel 不是可以设置和获取 QPixmap 对象吗,为什么要特地用一个私有成员来保存?因为 QLabel 上显示的图像,咱们一般会缩小一下再显示。经过缩小后的 QPixmap 对象,再重新放大就变得很模糊了。所以,QLabel::Pixmap 不保存原图。

在构造函数中,让这个标签组件支持放置。

MyLabel::MyLabel(QWidget *parent) : QLabel(parent) { this->setAcceptDrops(true); this->setStyleSheet("background-color: gray"); }

setAcceptDrops 开启 drop 支持。还有一个是 setStyleSheet,这里老周是用 QSS 来设置标签的背景颜色为难看的灰色。这是 Qt 搞的装X玩意儿,用起来有点像 HTML 中的 CSS。

又有伙伴问了,QLabel 不是有个带 text 参数的构造函数吗?对,不过这里不需要,咱们这个自定义组件不显示文本。

然后,实现 resizeEvent,当大小改变时,咱们也调整一下标签上的图像大小(其实是重新加载缩放过的图像)。

void MyLabel::resizeEvent(QResizeEvent *event) { if(!m_image.isNull()) { // 获取当前新调整的大小 QSize labelsize = event->size(); // 缩放图像 auto pixmap = m_image.scaled(labelsize, Qt::KeepAspectRatio, Qt::SmoothTransformation); // 重新设置图像 this->setPixmap(pixmap); } }

最后就是跟drop 有关的两个事件了。

void MyLabel::dragEnterEvent(QDragEnterEvent *event) { // 检查一下是不是所需要的数据 const QMimeData *data = event->mimeData(); if (data->hasUrls()) { event->acceptProposedAction(); } } void MyLabel::dropEvent(QDropEvent *event) { // 再次验证一下数据 const QMimeData *data = event->mimeData(); if (data->hasUrls()) { // 读数据 QList paths = data->urls(); if (paths.size() > 0) { QUrl p = paths.at(0); QString locfile = p.toLocalFile(); m_image.load(locfile); } // 缩放一下 auto pix = m_image.scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); this->setPixmap(pix); event->acceptProposedAction(); } }

dragEnter 的时候,只是看看有没有想要的数据,不读。读取是在 drop 事件中完成。但是为了防止概率 0.001% 的灵异事件发生,在 drop 事件处理时还要再检验一下数据是不是有效。

文件拖进来,一般是 URL 类型,获取到的对象是 QUrl 类型,它的格式是 file:///xxxxx,这个路径在 load 方法中加载不了,于是得用 toLocalFile 方法,将 URL 转换为本地文件路径,这样就能在 QPixmap::load 方法中加载图像了。

下面,定义一个窗口,实例化两个 MyLabel 组件,放在网格布局中第一行的两个单元格内。

/* 头文件 */ #pragma once #include #include "custlabel.h" #include class MyWindow : public QWidget { Q_OBJECT public: MyWindow(QWidget* parent=nullptr); private: MyLabel *lbImg1, *lbImg2; QLabel *lb1, *lb2; QGridLayout *layout; };

两个 QLabel 组件用来显示普通文本,咱们自己弄的 MyLabel 组件用来显示图片。QGridLayout 是布局用的,以网格形式布局(行、列)。

MyWindow::MyWindow(QWidget *parent) :QWidget(parent) { setWindowTitle("放置图像"); resize(450, 400); // 初始化 lb1 = new QLabel("美琪", this); lb2 = new QLabel("美雪", this); lb1->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); lb2->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed)); lbImg1 = new MyLabel(this); lbImg2 = new MyLabel(this); layout = new QGridLayout(this); // 布局 layout->addWidget(lbImg1, 0, 0); layout->addWidget(lbImg2, 0, 1); layout->addWidget(lb1, 1, 0); layout->addWidget(lb2, 1, 1); }

setSizePolicy 那两行是为了让 QLabel 组件的高度固定,因为 QGridLayout 这个王八不能设置固定的行高和列宽,所以只能出此下策了。

写上 main 函数。

int main(int argc, char* argv[]) { QApplication app(argc, argv); MyWindow win; win.show(); return QApplication::exec(); }

运行程序后,就可以把图片文件拖到两个 MyLabel 上了。注意左边是美琪,右边是美雪,下面的标签是她俩的名字。

 



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3